Skip to content

Conversation

kwasniew
Copy link
Contributor

@kwasniew kwasniew commented Aug 5, 2025

About the changes

Adding SSE streaming client support in this SDK similar to Node SDK.

  • When starting the SDK we can set the experimental mode to be streaming instead of default polling mode
  • I manually tested that disconnects are handled gracefully (this should be also tested in the SSE client itself)
  • Added tests showing how streaming works from this SDK perspective
  • Under the hood streaming events are handled by the yggdrasil engine so we don't need to test all the details as in the Node SDK
  • JRuby doesn't have support for one of the SSE client transitive dependencies so it falls back to polling
  • Also we don't store state to a backup file for now in streaming mode. It will be added once yggdrasil can expose full state to the SDKs

Important files

Discussion points

@unleash = Unleash::Client.new(
url: 'https://unleash.herokuapp.com/api',
custom_http_headers: { 'Authorization': '943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0' },
url: 'https://app.unleash-hosted.com/demo/api',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-using sdk-examples setup https://github.com/Unleash/unleash-sdk-examples/blob/main/Ruby/.env.example as heroku is deprecated

@kwasniew kwasniew force-pushed the streaming-support branch from 368230d to 7fb2b86 Compare August 5, 2025 10:13
Unleash.engine.register_custom_strategies(Unleash.configuration.strategies.custom_strategies)

Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine
Unleash.toggle_fetcher = Unleash::ToggleFetcher.new Unleash.engine unless Unleash.configuration.streaming_mode?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetcher makes initial HTTP call on instance creation. We don't want that for how with streaming to avoid cross locks between streaming client and polling client

:bootstrap_config,
:strategies,
:use_delta_api
:use_delta_api,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should deprecate this property as we have a standardized experimental mode in Node SDK that is either {type: 'streaming'} or {type: 'polling', format: 'delta'} or {type: 'polling', format: 'full'}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think having a standard with interface between language and SDK is too important here, most of the SDKs provide interfaces that are comfortable in that language.

I do agree that we should deprecate this and use your pattern though

self.toggle_engine.take_state(event.data)
end

# TODO: update backup file
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state engine needs to expose current state so we can save it . Both streaming and polling with delta will need it

puts ">> START streaming.rb"

@unleash = Unleash::Client.new(
url: 'https://app.unleash-hosted.com/demo/api',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

demo doesn't have streaming enabled so this needs to be changed to enterprise URL

spec.require_paths = ["lib"]
spec.required_ruby_version = ">= 2.7"

spec.add_dependency "ld-eventsource", "2.2.4" unless RUBY_ENGINE == 'jruby'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSE client has http dependency that doesn't work with jruby

).to be false
end

unless RUBY_ENGINE == 'jruby'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run those tests for non jruby platforms

return nil if RUBY_ENGINE == 'jruby'

begin
require 'ld-eventsource'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't let this dependency spread throughout the code

@coveralls
Copy link

coveralls commented Aug 5, 2025

Pull Request Test Coverage Report for Build 16880273865

Details

  • 95 of 109 (87.16%) changed or added relevant lines in 5 files are covered.
  • 1 unchanged line in 1 file lost coverage.
  • Overall coverage decreased (-1.1%) to 94.32%

Changes Missing Coverage Covered Lines Changed/Added Lines %
lib/unleash/configuration.rb 13 14 92.86%
lib/unleash/util/event_source_wrapper.rb 7 9 77.78%
lib/unleash/streaming_client_executor.rb 41 45 91.11%
lib/unleash/streaming_event_processor.rb 22 29 75.86%
Files with Coverage Reduction New Missed Lines %
lib/unleash/configuration.rb 1 96.7%
Totals Coverage Status
Change from base Build 16671337925: -1.1%
Covered Lines: 548
Relevant Lines: 581

💛 - Coveralls

end

def start
self.mutex.synchronize do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar synchronization pattern to toggle_fetcher so that we guard running property

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to guard running? That should only ever be a problem if two threads are calling start and stop on the same instance in quick succession, surely?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No mutex needed in the scheduler for SSEs - lifecycle methods called from single thread. But in the event processor event processing happens from EventSource background threads that could potentially be concurrent so keeping it there

@ivarconr ivarconr moved this from New to In Progress in Issues and PRs Aug 6, 2025
Copy link
Member

@sighphyre sighphyre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! Come grab me next week and we can beat Yggdrasil into shape so you can finish this off!

:bootstrap_config,
:strategies,
:use_delta_api
:use_delta_api,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think having a standard with interface between language and SDK is too important here, most of the SDKs provide interfaces that are comfortable in that language.

I do agree that we should deprecate this and use your pattern though

end

def streaming_mode?
return false if RUBY_ENGINE == 'jruby'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider raising an exception here or logging an error. I would be very annoyed if an SDK I used didn't do what I told it and quietly did something different

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair point

end

def start
self.mutex.synchronize do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to guard running? That should only ever be a problem if two threads are calling start and stop on the same instance in quick succession, surely?

@@ -1,15 +1,17 @@
require 'unleash/configuration'
require 'unleash/toggle_fetcher'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there's a missing somewhere here and I think that's related to the difference between the toggle fetcher internals and LD's event source lib. The toggle fetching code splits the logic for fetching toggles and the concurrency responsibility into two classes, whereas with the event source lib it's a single entity. This means we can't treat them like interchangable ducks and I really think we should be able to do that. It feels very wrong to have a :streaming_client and :fetcher_scheduled_executor exposed as accesors when we can only ever have one of these and they do the same job in different ways. It's leading to a bunch of places in this PR where we're checking which we and making local decisions. If we created something like a fetch_client that composed of both the toggle_fetcher and scheduled_executor which exposes start and stop methods we could make a lot of that go away

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the root cause is who's driving the new updates: scheduler or streaming client handler. I will play around with a different split of responsibilities (as I did in the Java SDK) on Monday but I'm not sure we can have a drop-in replacement here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You comment was spot on. Thank you for that. I managed to create streaming_client_executor and streaming_event_processor. streaming_client_executor can play a role of a fetcher_scheduled_executor (kept the same field name in attr jus tin case someone depends on it). Please let me know if you like it more now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it!

module Unleash
module Util
module EventSourceWrapper
def self.client
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm a bit tempted to fail hard here rather than fall back to polling. I'm kinda okay with errors that happen on startup and I think it's better to force folks to set their stuff up correctly. We can always relax it later but if we do this now, I doubt we can enforce it later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

@sighphyre sighphyre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking awesome. Gonna be great to get this one done!

@github-project-automation github-project-automation bot moved this from In Progress to Approved PRs in Issues and PRs Aug 11, 2025
@kwasniew kwasniew merged commit 0f21509 into main Aug 11, 2025
44 checks passed
@kwasniew kwasniew deleted the streaming-support branch August 11, 2025 13:09
@github-project-automation github-project-automation bot moved this from Approved PRs to Done in Issues and PRs Aug 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants